Add Price and Extended price to Project Bom#1221
Add Price and Extended price to Project Bom#1221whinis wants to merge 2 commits intoPart-DB:masterfrom
Conversation
166bcb5 to
35262a2
Compare
|
Thanks, The PricedetailHelper has an calculateAvg() price to calculate an average price across all suppliers, it also handles currency conversions correctly. Unfortunatley the whole price infrastructure in Part-DB is quite limited and ideally would need some heavy rewrites, with some more powerful functions to make things like this easier... |
Just a random thought: The complicated thing about pricing: it won't reflect the actual price we bought parts for & sometimes our stock even might contain the same part bought for different prices. I would love the price estimation to reflect what I'm able to buy the parts now for: Long story short: I know prices in PartDB are complicated! |
Thats what the PR essentially does or theoretically does. I could force it but it currently gets the first price which atleast in my setup tends to be qty 1 price. |
|
@whinis I implemented the changes @jbtronics asked for. |
|
My previous comment can be ignored - I realised how average price works! :) |
|
@whinis If you want to, you can commit my patch integrating the requested changes. The changes greatly simplify the price calculation! From 44a9aa16b312f64b83777efd8c369a8ce3805310 Mon Sep 17 00:00:00 2001
From: MayNiklas <info@niklas-steffen.de>
Date: Fri, 13 Mar 2026 21:15:25 +0100
Subject: [PATCH] project prices: use calculateAvg
diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php
index 3acef03f..ec1f5ff4 100644
--- a/src/DataTables/ProjectBomEntriesDataTable.php
+++ b/src/DataTables/ProjectBomEntriesDataTable.php
@@ -39,12 +39,14 @@ use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
+use App\Services\Parts\PricedetailHelper;
use Brick\Math\BigDecimal;
class ProjectBomEntriesDataTable implements DataTableTypeInterface
{
public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper,
- protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
+ protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter,
+ protected PricedetailHelper $pricedetailHelper)
{
}
@@ -183,62 +185,18 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
->add('price', TextColumn::class, [
'label' => 'project.bom.price',
'render' => function ($value, ProjectBOMEntry $context) {
- // Let's attempt to get the part, if we don't we will just assume zero
- $part = $context->getPart();
- $price = BigDecimal::zero();
- $pricedetails = null;
- $order = null;
- // Check if we get a part and get the first order if so
- // if not see if there is a non-part price set
- if($part) {
- $order = $context->getPart()->getOrderdetails()->first();
- } else if($context->getPrice() !== null) {
- $price = $context->getPrice();
- }
-
- // check if there is an order, if so get the first pricedetail of the order
- if($order!==null && $order !== false) {
- $pricedetails = $order->getPricedetails()->first();
- }
-
- // check if there is a pricedetail, if so get the first price
- if($pricedetails !== null && $pricedetails !== false) {
- $price = $pricedetails->getPrice();
- }
+ $price = $this->getBomEntryUnitPrice($context);
- // return the price
- return htmlspecialchars(number_format($price->toFloat(),2));
+ return htmlspecialchars(number_format($price->toFloat(), 2));
},
'visible' => false,
])
->add('ext_price', TextColumn::class, [
'label' => 'project.bom.ext_price',
'render' => function ($value, ProjectBOMEntry $context) {
- // Let's attempt to get the part, if we don't we will just assume zero
- $part = $context->getPart();
- $price = BigDecimal::zero();
- $pricedetails = null;
- $order = null;
- // Check if we get a part and get the first order if so
- // if not see if there is a non-part price set
- if($part) {
- $order = $context->getPart()->getOrderdetails()->first();
- } else if($context->getPrice() !== null) {
- $price = $context->getPrice();
- }
-
- // check if there is an order, if so get the first pricedetail of the order
- if($order!==null && $order !== false) {
- $pricedetails = $order->getPricedetails()->first();
- }
+ $price = $this->getBomEntryUnitPrice($context);
- // check if there is a pricedetail, if so get the first price
- if($pricedetails !== null && $pricedetails !== false) {
- $price = $pricedetails->getPrice();
- }
-
- // return the price
- return htmlspecialchars(number_format($price->toFloat() * $context->getQuantity(),2));
+ return htmlspecialchars(number_format($price->toFloat() * $context->getQuantity(), 2));
},
])
@@ -268,6 +226,16 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
]);
}
+ private function getBomEntryUnitPrice(ProjectBOMEntry $entry): BigDecimal
+ {
+ if ($entry->getPart() instanceof Part) {
+ return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $entry->getQuantity()) ?? BigDecimal::zero();
+ }
+
+ return $entry->getPrice() ?? BigDecimal::zero();
+ }
+
+
private function getQuery(QueryBuilder $builder, array $options): void
{
$builder->select('bom_entry') |
|
@MayNiklas Alright I incorporated that. It does look nicer in this case. |
|
@jbtronics this PR should be ready for a re-review :) |
|
@jbtronics I just noticed, that |
MayNiklas
left a comment
There was a problem hiding this comment.
I just added two suggestions I noticed this morning :)
- prices should maybe be rounded up, since 0,00€ parts are realistically more 0,01€ stan 0,00€
- in cases where getMinOrderAmount > amount, we previously returned NULL. With my suggestion we would continue to work with the minimum ammount that can be ordered.
| return htmlspecialchars(number_format($price->toFloat() * $context->getQuantity(),2)); | ||
| }, |
There was a problem hiding this comment.
| return htmlspecialchars(number_format($price->toFloat() * $context->getQuantity(),2)); | |
| }, | |
| return htmlspecialchars(number_format($this->roundUpDisplayedPrice($price)->toFloat(), 2)); | |
| }, |
| $price = $this->getBomEntryUnitPrice($context); | ||
|
|
||
| // return the price | ||
| return htmlspecialchars(number_format($price->toFloat(),2)); |
There was a problem hiding this comment.
How about rounding prices up to the full cent?
This might be especially helpful for very cheap parts:
Currently resistors / caps might be shown as 0,00€, even while not being free.
| return htmlspecialchars(number_format($price->toFloat(),2)); | |
| return htmlspecialchars(number_format($this->roundUpDisplayedPrice($price)->toFloat(), 2)); |
| if ($entry->getPart() instanceof Part) { | ||
| return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $entry->getQuantity()) ?? BigDecimal::zero(); | ||
| } | ||
|
|
||
| return $entry->getPrice() ?? BigDecimal::zero(); |
There was a problem hiding this comment.
We previously not correctly handled cases with amount < minimum order amount.
This should fix it!
| if ($entry->getPart() instanceof Part) { | |
| return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $entry->getQuantity()) ?? BigDecimal::zero(); | |
| } | |
| return $entry->getPrice() ?? BigDecimal::zero(); | |
| if ($entry->getPart() instanceof Part) { | |
| $amount = $entry->getQuantity(); | |
| $minOrderAmount = $this->pricedetailHelper->getMinOrderAmount($entry->getPart()); | |
| if ($minOrderAmount !== null) { | |
| $amount = max($amount, $minOrderAmount); | |
| } | |
| return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $amount) ?? BigDecimal::zero(); | |
| } | |
| return $entry->getPrice() ?? BigDecimal::zero(); |
|
|
||
| return $entry->getPrice() ?? BigDecimal::zero(); | ||
| } | ||
|
|
There was a problem hiding this comment.
I just forgot to add roundUpDisplayedPrice
| private function roundUpDisplayedPrice(BigDecimal $price): BigDecimal | |
| { | |
| return $price->toScale(2, RoundingMode::UP); | |
| } |
This seems to be one of the most requested changes and I also needed this particular feature.
requested in #603 and others in discussion. So this allows one to see the price of each item in the bom if it exists in any way. It currently selects just the first supplier and the first (likely most expensive) unit price for the part.